using System;
using System.IO;
using System.Configuration;
using System.Linq;
using System.Text;
using BSDS.Core.Data.Local;
using BSDS.Core.Infrastructure.Logging;
using BSDS.Core.Model.Data;
using BSDS.Core.Model.Conversion;
using BSDS.Core.Model.Services;
using BSDS.Core.Model.Services.Geocoding;
using BSDS.Core.Model.Services.Jobs;
using BSDS.Core.Model.Services.Jobs.Core;
using BSDS.Core.Model.Services.Uploading;
using BSDS.Core.Model.Conversion.Processing;

namespace BSDS.Commands.Commands.Implementation
{
    public class GeocodeCommand : CommandBase
    {
        private readonly GeocodingService geoService;
        private readonly DataRepository dataRepository;
        private readonly UploadService uploadService;
        private readonly AccountService accountService;
        private readonly JobRepository jobRepository;
        private readonly JobService jobService;
        private readonly Logger logger;
        private readonly GeocodeCommandConfiguration config;
        private readonly DateTime startTime;

        public override string Description
        {
            get { return CreateDescription(); }
        }

        public GeocodeCommand(GeocodingService geoService, AccountService accountService, DataRepository dataRepository,
                              UploadService uploadService, JobRepository jobRepository,
                              JobService jobService, Logger logger, GeocodeCommandConfiguration config)
        {
            this.geoService = geoService;
            this.accountService = accountService;
            this.dataRepository = dataRepository;
            this.uploadService = uploadService;
            this.jobRepository = jobRepository;
            this.jobService = jobService;
            this.logger = logger;

            this.config = config;

            startTime = DateTime.Now;
        }

        public override CommandResult Execute(ExecutionContext context)
        {
            config.Validate();

            // Validate file if a drop filename prefix is defined
            if (!string.IsNullOrEmpty(config.DropFilenamePrefix) && !Path.GetFileNameWithoutExtension(config.InputFilename).StartsWith(config.DropFilenamePrefix))
            {
                string message = "Filename must start with \"" + config.DropFilenamePrefix + "\" : " + config.InputFilename;
                logger.Log("File Ingored. " + message);

                return CommandResult.Error("Unsupported file was dropped", message);
            }

            // Load input data file
            var data = dataRepository.LoadFromFile(config.InputFilename, config.ConvertMode);

            // Assign the entity type name to the dataset
            if (!String.IsNullOrEmpty(config.EntityTypeName))                                                               
            {
                data.EntityTypeName = config.EntityTypeName;
            }

            // Merge columns if required
            if (config.MergeConfigurationFile != string.Empty)
            {
                //MergeColumnsProcessor.CreateSampleData();
                MergeColumnsProcessor.LoadConfiguration(config.MergeConfigurationFile);
                MergeColumnsProcessor.MergeColumns(data);
            }

            // Delete columns if required
            if (config.DeleteConfigurationFile != string.Empty)
            {
                //DeleteColumnsProcessor.CreateSampleData();
                DeleteColumnsProcessor.LoadConfiguration(config.DeleteConfigurationFile);
                DeleteColumnsProcessor.DeleteColumns(data);
            }

            // Insert columns if required
            if (config.InsertConfigurationFile != string.Empty)
            {
                //InsertColumnsProcessor.CreateSampleData();
                InsertColumnsProcessor.LoadConfiguration(config.InsertConfigurationFile);
                InsertColumnsProcessor.InsertColumns(data);
            }

            // Load the geocode override table
            string geocodeOverridesTable = "GeocodeOverrides.csv";
            if (File.Exists(geocodeOverridesTable)) 
            {
                var geocodeEntities = dataRepository.LoadFromFile(geocodeOverridesTable, FormatConvertMode.DontConvert).Entities;
                // Insert geocode values into the original dataset
                data.UpdateGeocoding(geocodeEntities);
                logger.Log("Loaded " + geocodeEntities.Count() + " entities from Geocode Override table: " + geocodeOverridesTable);
            }
            logger.Log("Geocode overrides table not found (skipping): " + geocodeOverridesTable);

            JobState jobState;

            if (config.ResumeCompletedJob)
            {
                jobState = geoService.QueryState(config.CompletedJobId);
                return ProcessGeocodingResults(data, jobState);
            }

            if (!data.GeocodingNeeded(config.GeocodingMode))
            {
                logger.Log("No entities for geocoding. All geoinformation already available.");

                SaveData(data);
                UploadIfRequested(config.OutputFilename);

                return CommandResult.Success();
            }

            var job = geoService.ScheduleGeocoding(data, config.GeocodingMode);
            job.Command = context.CommandInfo.ToString();
            jobRepository.Save(job);

            jobState = jobService.WaitForEnd(job.Id, geoService);

            return ProcessGeocodingResults(data, jobState);
        }

        private CommandResult ProcessGeocodingResults(BsdsData data, JobState jobState)
        {
            var commandResult = CommandResult.Success();
            var geocodingDetails = new StringBuilder();

            if (!jobState.DataAvailable)
            {
                var failureDetails = geoService.GetFailureDetials(jobState);
                logger.Log(string.Format("Failure: {0}.", failureDetails));
                return CommandResult.Error("Geocoding error.", failureDetails);
            }

            // Get all geocoding results from Bing SDS
            var job = jobRepository.Get<GeocodeJob>(jobState.Id);
            var BSDSGeocodingResult = geoService.DownloadGecodingResults(job.Id);
            var BSDSGeocodedEntities = BSDSGeocodingResult.GetAllEntities();

            // Update the original data with the geocode results
            if (job.Mode == GeocodingMode.Reverse)
            {
                data.UpdateReverseGeocoding(BSDSGeocodedEntities);
            }
            else
            {
                // Update the original dataset with the geocode results
                data.UpdateGeocoding(BSDSGeocodedEntities);

                // Find all locations that did not geocode well or at all
                var impreciseOrBadGeocodeEntities = new GeocodingResult(BSDSGeocodingResult.GetImpreciseOrBadEntities());
                var regeocodeTheseEntities = data.GetMatchingEntities(impreciseOrBadGeocodeEntities);

                // Try to improve the geocoding by using the Bing Maps Locations API
                BSDSGeocodingResult = geoService.GeocodeEntitiesIndividually(regeocodeTheseEntities);
                var regeocodedEntites = BSDSGeocodingResult.GetAllEntities();

                data.UpdateGeocoding(regeocodedEntites); // TODO: Very unlikely, but it may be safer to make sure the re-geocoded entity is not a lower confidence than the originally geocoded entity.

                // Check and fix duplicate addresses in the same file that were geocoded inconsistently.
                data.fixInconsistentGeocodes();
            }

            // The logic below assumes we have re-geocoded all imprecise or failing entities
            if (!BSDSGeocodingResult.AllEntitiesHighConfidence())
            {
                geocodingDetails.Append("IMPRECISE ENTITIES:").Append(Environment.NewLine);
                var impreciseEntitiesIds = BSDSGeocodingResult.GetImpreciseEntities().Select(e => e.PrimaryKeyValue).ToArray();
                geocodingDetails.Append(string.Join(",", impreciseEntitiesIds)).Append(Environment.NewLine + Environment.NewLine);
                commandResult = CommandResult.Warning("Geocoding was imprecise or ambiguous for some entities", geocodingDetails.ToString());
            }

            if (!BSDSGeocodingResult.AllEntitiesValid())
            {
                geocodingDetails.Append("FAILED ENTITIES:").Append(Environment.NewLine);
                var invalidEntitiesIds = BSDSGeocodingResult.GetFailedEntities().Select(e => e.PrimaryKeyValue).ToArray();
                geocodingDetails.Append(string.Join(",", invalidEntitiesIds)).Append(Environment.NewLine + Environment.NewLine);
                // The following command result short message takes prececence, but we will provide cumultive detail results
                commandResult = CommandResult.Warning("Geocoding has failed for some entities", geocodingDetails.ToString());
            }

            logger.Log(geocodingDetails.ToString());
            logger.Log(string.Format("Saving geocoding results to {0}.", config.OutputFilename));

            SaveData(data);

            UploadIfRequested(config.OutputFilename);

            return commandResult;
        }

        private void SaveData(BsdsData data)
        {
            var outputFormat = config.OutputFormat == DataFormat.OriginalFormat ? data.OriginalFormat : config.OutputFormat;
            if (config.Portal)
            {
                if (outputFormat != DataFormat.Tab && outputFormat != DataFormat.Pipe)
                {
                    // TODO: Shouldn't we be checking this before doing any work?
                    throw new ArgumentException("/portal option can be used only with /toTab or /toPipe");
                }
                dataRepository.SaveInPortalFormat(data, config.OutputFilename, outputFormat);
            }
            else
            {
                dataRepository.SaveToFile(data, config.OutputFilename, outputFormat);
            }
        }

        private void UploadIfRequested(string filename)
        {
            if (config.UploadAfterMerging)
            {
                var uploadData = dataRepository.LoadFromFile(filename, FormatConvertMode.DontConvert);
                var outputFormat = config.OutputFormat == DataFormat.OriginalFormat ? uploadData.OriginalFormat : config.OutputFormat; 
                var job = uploadService.ScheduleUpload(uploadData, config.DataSourceName, outputFormat, config.EntityTypeName, config.QueryKey);

                jobService.WaitForEnd(job.Id, uploadService);
            }
        }

        private string CreateDescription()
        {
            var builder = new StringBuilder();

            builder.Append("GEOCODE START TIME: " + startTime.ToLongTimeString()).Append(Environment.NewLine)
                   .Append("DATA SOURCE NAME: " + config.DataSourceName).Append(Environment.NewLine);

            if (config.UploadAfterMerging && accountService.DataSourceExists(config.DataSourceName))
            {
                string publishDate = accountService.GetDataSourceInformation(config.DataSourceName).LastUpdate;
                builder.Append("LAST PUBLISH DATE: " + publishDate);
            }

            return builder.ToString();
        }
    }

    public class GeocodeCommandConfiguration
    {
        public string InputFilename { get; private set; }
        public GeocodingMode GeocodingMode { get; private set; }
        public bool StoreInOutputFile { get; set; }
        public string OutputFilename { get; set; }
        public DataFormat OutputFormat { get; private set; }
        public bool Portal { get; private set; }
        public FormatConvertMode ConvertMode { get; private set; }
        public bool UploadAfterMerging { get; private set; }
        public string DataSourceName { get; private set; }
        public string EntityTypeName { get; private set; }
        public string QueryKey { get; private set; }
        public bool ResumeCompletedJob { get; private set; }
        public string CompletedJobId { get; private set; }
        public string DropFilenamePrefix { get; private set; }
        public string MergeConfigurationFile { get; private set; }
        public string DeleteConfigurationFile { get; private set; }
        public string InsertConfigurationFile { get; private set; }

        public GeocodeCommandConfiguration(string inputFilename, GeocodingMode geocodingMode, bool storeInOutputFile, 
            string outputFilename, bool portal, FormatConvertMode convertMode, DataFormat outputFormat, bool uploadAfterMerging, 
            string dataSourceName, string entityTypeName, string queryKey, bool resumeCompletedJob, string jobId,
            string dropFilenamePrefix, string mergeConfigurationFile, string deleteConfigurationFile, string insertConfigurationFile)
        {
            InputFilename = inputFilename;
            GeocodingMode = geocodingMode;
            StoreInOutputFile = storeInOutputFile;
            OutputFilename = outputFilename;
            OutputFormat = outputFormat;
            Portal = portal;
            ConvertMode = convertMode;
            UploadAfterMerging = uploadAfterMerging;
            DataSourceName = dataSourceName;
            EntityTypeName = entityTypeName;
            QueryKey = queryKey;
            ResumeCompletedJob = resumeCompletedJob;
            CompletedJobId = jobId;
            DropFilenamePrefix = dropFilenamePrefix;
            MergeConfigurationFile = mergeConfigurationFile;
            DeleteConfigurationFile = deleteConfigurationFile;
            InsertConfigurationFile = insertConfigurationFile;
        }

        public void Validate()
        {
            if (string.IsNullOrEmpty(OutputFilename))
            {
                throw new ConfigurationErrorsException("/outfile parameter is required for /geocode command");
            }
        }
    }
}